iT邦幫忙

2023 iThome 鐵人賽

DAY 27
0

前面我們已經討論過了大多數 Ktor 框架提供的功能。

從 v2.2.0 開始,Ktor 還提供了一個功能,就是允許開發者自己開發客製化的套件。

今天我們來看看開發的方式,以及 Ktor 如何允許使用者自己開發客製化套件。

根據官網教學,可以先寫一個 SimplePlugin

import io.ktor.server.application.*

val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
    println("SimplePlugin is installed!")
}

定義好 SimplePlugin 之後,我們就可以像安裝其他套件一樣

fun Application.module() {
    install(SimplePlugin)
}

這樣一來,運作這個套件時,我們就會看到 "SimplePlugin is installed!"

我們來看看 createApplicationPlugin 的實作

/**
 * Creates an [ApplicationPlugin] that can be installed into an [Application].
 *
 * The example below creates a plugin that prints a requested URL each time your application receives a call:
 * ```
 * val RequestLoggingPlugin = createApplicationPlugin("RequestLoggingPlugin") {
 *      onCall { call ->
 *          println(call.request.uri)
 *      }
 * }
 *
 * application.install(RequestLoggingPlugin)
 * ```
 *
 * You can learn more from [Custom plugins](https://ktor.io/docs/custom-plugins.html).
 *
 * @param name A name of a plugin that is used to get an instance of the plugin installed to the [Application].
 * @param body Allows you to define handlers ([onCall], [onCallReceive], [onCallRespond] and so on) that
 * can modify the behaviour of an [Application] where your plugin is installed.
 **/
public fun createApplicationPlugin(
    name: String,
    body: PluginBuilder<Unit>.() -> Unit
): ApplicationPlugin<Unit> = createApplicationPlugin(name, {}, body)
/**
 * Creates an [ApplicationPlugin] that can be installed into an [Application].
 *
 * The example below creates a plugin that prints a requested URL each time your application receives a call:
 * ```
 * val RequestLoggingPlugin = createApplicationPlugin("RequestLoggingPlugin") {
 *      onCall { call ->
 *          println(call.request.uri)
 *      }
 * }
 *
 * application.install(RequestLoggingPlugin)
 * ```
 *
 * You can learn more from [Custom plugins](https://ktor.io/docs/custom-plugins.html).
 *
 * @param name A name of a plugin that is used to get its instance.
 * @param createConfiguration Defines how the initial [PluginConfigT] of your new plugin can be created.
 * Note that it may be modified later when a user of your plugin calls [Application.install].
 * @param body Allows you to define handlers ([onCall], [onCallReceive], [onCallRespond] and so on) that
 * can modify the behaviour of an [Application] where your plugin is installed.
 *
 **/
public fun <PluginConfigT : Any> createApplicationPlugin(
    name: String,
    createConfiguration: () -> PluginConfigT,
    body: PluginBuilder<PluginConfigT>.() -> Unit
): ApplicationPlugin<PluginConfigT> = object : ApplicationPlugin<PluginConfigT> {
    override val key: AttributeKey<PluginInstance> = AttributeKey(name)

    override fun install(
        pipeline: Application,
        configure: PluginConfigT.() -> Unit
    ): PluginInstance {
        return createPluginInstance(pipeline, pipeline, body, createConfiguration, configure)
    }
}

到這邊會先將套件名稱的 name 定義成 AttributeKey

/**
 * Specifies a key for an attribute in [Attributes]
 * @param T is a type of the value stored in the attribute
 * @param name is a name of the attribute for diagnostic purposes. Can't be blank
 */
public class AttributeKey<T : Any>(public val name: String) {
    init {
        if (name.isEmpty()) {
            throw IllegalStateException("Name can't be blank")
        }
    }

    override fun toString(): String = "AttributeKey: $name"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as AttributeKey<*>

        if (name != other.name) return false

        return true
    }

    override fun hashCode(): Int {
        return name.hashCode()
    }
}

createApplicationPlugin.install 內的 createPluginInstance 定義則是

private fun <
    PipelineT : ApplicationCallPipeline,
    PluginConfigT : Any
    > Plugin<PipelineT, PluginConfigT, PluginInstance>.createPluginInstance(
    application: Application,
    pipeline: ApplicationCallPipeline,
    body: PluginBuilder<PluginConfigT>.() -> Unit,
    createConfiguration: () -> PluginConfigT,
    configure: PluginConfigT.() -> Unit
): PluginInstance {
    val config = createConfiguration().apply(configure)

    val currentPlugin = this
    val pluginBuilder = object : PluginBuilder<PluginConfigT>(currentPlugin.key) {
        override val application: Application = application
        override val pipeline: ApplicationCallPipeline = pipeline
        override val pluginConfig: PluginConfigT = config
    }

    pluginBuilder.setupPlugin(body)
    return PluginInstance(pluginBuilder)
}

這邊會實作出一個 PluginBuilder 來協助設置

PluginBuilder 的註解與簽名如下

/**
 * A utility class to build an [ApplicationPlugin] instance.
 **/
@KtorDsl
@Suppress("UNUSED_PARAMETER", "DEPRECATION")
public abstract class PluginBuilder<PluginConfig : Any> internal constructor(
    internal val key: AttributeKey<PluginInstance>
) 

裡面的參數則是

/**
 * A reference to the [Application] where the plugin is installed.
 */
public abstract val application: Application

/**
 * A configuration of the current plugin.
 */
public abstract val pluginConfig: PluginConfig

/**
 * A pipeline [PluginConfig] for the current plugin.
 * See [Pipelines](https://ktor.io/docs/pipelines.html) for more information.
 **/
internal abstract val pipeline: ApplicationCallPipeline

/**
 * Allows you to access the environment of the currently running application where the plugin is installed.
 **/
public val environment: ApplicationEnvironment? get() = pipeline.environment

可以看到,這裡面定義了 applicationpipeline 等等,這部分在前面接收請求或者送出回傳時都會使用到。

另外也定義了 environment = pipeline.environment

接著還定義了許多的列表,設置各種的攔截

internal val callInterceptions: MutableList<CallInterception> = mutableListOf()  
  
internal val onReceiveInterceptions: MutableList<ReceiveInterception> = mutableListOf()  
  
internal val onResponseInterceptions: MutableList<ResponseInterception> = mutableListOf()  
  
internal val afterResponseInterceptions: MutableList<ResponseInterception> = mutableListOf()  
  
internal val hooks: MutableList<HookHandler<*>> = mutableListOf()

然後我們就可以用 Builder.setupPlugin 來設置套件的行為

private fun <Configuration : Any, Builder : PluginBuilder<Configuration>> Builder.setupPlugin(
    body: Builder.() -> Unit
) {
    apply(body)

    callInterceptions.forEach {
        it.action(pipeline)
    }

    onReceiveInterceptions.forEach {
        it.action(pipeline.receivePipeline)
    }

    onResponseInterceptions.forEach {
        it.action(pipeline.sendPipeline)
    }

    afterResponseInterceptions.forEach {
        it.action(pipeline.sendPipeline)
    }

    hooks.forEach { it.install(pipeline) }
}

這樣一來,我們就定義完了一個 ApplicationPlugin

能夠透過 Pipeline.install 進行安裝

/**
 * Installs a [plugin] into this pipeline, if it is not yet installed.
 */
public fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.install(
    plugin: Plugin<P, B, F>,
    configure: B.() -> Unit = {}
): F {
    if (this is Route && plugin is BaseRouteScopedPlugin) {
        return installIntoRoute(plugin, configure)
    }

    val registry = pluginRegistry
    return when (val installedPlugin = registry.getOrNull(plugin.key)) {
        null -> {
            try {
                val installed = plugin.install(this, configure)
                registry.put(plugin.key, installed)
                // environment.log.trace("`${plugin.name}` plugin was installed successfully.")
                installed
            } catch (t: Throwable) {
                // environment.log.error("`${plugin.name}` plugin failed to install.", t)
                throw t
            }
        }
        plugin -> {
            // environment.log.warning("`${plugin.name}` plugin is already installed")
            installedPlugin
        }
        else -> {
            throw DuplicatePluginException(
                "Please make sure that you use unique name for the plugin and don't install it twice. " +
                    "Conflicting application plugin is already installed with the same key as `${plugin.key.name}`"
            )
        }
    }
}

這段我們在前面看安裝 route 時已經看過,不過這次不是安裝 route

也就是

val installed = plugin.install(this, configure)
registry.put(plugin.key, installed)
// environment.log.trace("`${plugin.name}` plugin was installed successfully.")
installed

到這邊,我們概略地看過怎麼定義一個客製化套件

以及 install 該套件時會發生什麼事情。

明天我們再來看看客製化套件還可以做哪些事情


上一篇
Day 26:webSocket Client 的撰寫以及實作
下一篇
Day 28:自定義套件的 onCall、onCallReceive、onCallRespond
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言